package net.ojbk.twobox.server;

import net.ojbk.twobox.pojo.ClientInfo;
import net.ojbk.twobox.pojo.ClientSession;
import net.ojbk.twobox.pojo.TcpMessage;
import org.apache.log4j.Logger;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;

/**
 * PACKAGE_NAME: net.ojbk.twobox.server;
 * Description:
 * 1、与用户建立一个长连接
 * 2、返回当前用户系列
 * 3、TCP打洞通讯
 */
public class CenterServer implements Runnable {
    // 其他
    private static Logger logger = Logger.getLogger(CenterServer.class);
    // 核心
    private ServerSocket serverSocket;
    private static int serverPort = 9999;
    // 相关公共数据维护
    private Map<String, ClientSession> sessionMap;
    private List<ClientInfo> clientInfos;

    public CenterServer() throws IOException {
        // 创建socket
        serverSocket = new ServerSocket();
        serverSocket.setSoTimeout(30 * 60 * 1000); // socket 会话超时时间设定
        // backlog：requested maximum length of the queue of incoming connections.
        System.out.println(Inet4Address.getLocalHost());
        serverSocket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), serverPort));
        logger.info("中央服务：启动成功。");
        // 数据结构初始化
        sessionMap = new HashMap<>();
        clientInfos = new ArrayList<>(); // 供查询Client用户，存储用户的信息列表
    }

    public static void main(String[] args) throws IOException {
        new Thread(new CenterServer()).start();
    }

    @Override
    public void run() {
        while (true) { // 多线程用来监控client的连接请求。连接后，单独再为其创建一个线程用于处理其发来的信息。
            try {
                Socket socket = serverSocket.accept();
                // 构造维护的数据结构
                final ClientSession clientSession = new ClientSession();
                clientSession.uuid = UUID.randomUUID().toString();
                final ClientInfo clientInfo = new ClientInfo(socket.getInetAddress().getHostAddress(), socket.getPort(), clientSession.uuid, socket.getInetAddress().getHostName());
                clientInfos.add(clientInfo);
                clientSession.clientInfo = clientInfo;
                clientSession.socket = socket;
                clientSession.objectInputStream = new ObjectInputStream(socket.getInputStream());
                clientSession.objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
                sessionMap.put(clientSession.uuid, clientSession);
                // 维护信息记录完毕
                logger.info("新Client连接：" + clientInfo);
                // 创建属于该Socket的接受信息与处理的线程
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            while (clientSession.isActive) { //closeSocket(clientSession)方法，会置此参数false，用于关闭此线程
                                TcpMessage tcpMessage = (TcpMessage) clientSession.objectInputStream.readObject();
                                logger.debug("收到消息：[" + clientInfo.ip + ":" + clientInfo.port + "]：" + tcpMessage);
                                // 处理请求
                                requestHandle(clientSession, tcpMessage);
                            }
                            logger.debug("线程结束：" + clientInfo.ip + ":" + clientInfo.port);
                        } catch (IOException | ClassNotFoundException e) {
                            // 关闭连接
                            try {
                                closeSocket(clientSession);
                            } catch (IOException ex) {
                                ex.printStackTrace();
                            }
                        }
                    }
                }).start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * client的request处理方法。
     * <p>
     * 通讯代码
     * 一、centerServer收到 的信息
     * 1 获取当前所有已连接客户的信息
     * 2 请求连接某用户，收到的是目标用户的uuid
     * 3 B说："这是为A（UUID）准备的socket"。中心分别告诉A\B 说可以开始了，然后断开与A/B的连接（补充：socket断开，两边都要操作）
     * 4 一条文字消息
     * 5 获取外网IP与端口
     * 6 请求断开连接
     * <p>
     * 二、client收到centerServer 的信息
     * 1001 返回当前所有已连接客户的信息
     * 1002 有人请求TCP与我连接
     * 1003 client请求5后，返回的外网ip与端口
     * <p>
     * 三、对等方(欲建立p2p连接的Socket)收到 的信息
     * 2001 Center通知A："我联系上B了，现在给你B的IP/Port，你可以连接B了。"
     * 2002 Center通知B："A已知道你准备好了，你可以连接A了"
     * 2003 一条文字消息
     */
    public void requestHandle(ClientSession clientSession, TcpMessage tcpMessage) throws IOException {
        switch (tcpMessage.code) {
            case 1: // 获取当前所有已连接客户的信息
                logger.debug("处理请求：[" + clientSession.clientInfo.ip + ":" + clientSession.clientInfo.port + "]：" + "获取所有已连接客户的信息");
                TcpMessage response = new TcpMessage();
                response.code = 1001;
                ClientInfo[] clientInfoArray = new ClientInfo[clientInfos.size()];
                clientInfos.toArray(clientInfoArray);
                response.data = clientInfoArray;
                clientSession.objectOutputStream.writeObject(response);
                clientSession.objectOutputStream.flush();
                logger.debug("发送消息：目标：[" + clientSession.clientInfo.ip + ":" + clientSession.clientInfo.port + "]，信息：" + response);
                break;
            case 2: // A：请求连接某用户，收到的是目标用户的uuid
                String Buuid = (String) tcpMessage.data;
                logger.debug("处理请求：[" + clientSession.clientInfo.ip + ":" + clientSession.clientInfo.port + "]：" + "请求连接B(" + Buuid + ")");
                // 通知B
                TcpMessage toB = new TcpMessage();
                toB.code = 1002;
                List<Object> data = new ArrayList<>(); // 之前单独测试List不能序列化，但是这里可以，那我就先留着这个隐患，以便于以后如果出错从再深入学习！
                data.add(clientSession.uuid);
                data.add(clientSession.clientInfo.ip);
                data.add(clientSession.clientInfo.port);
                toB.data = data;
                // 发送到B
                ClientSession BSession = sessionMap.get(Buuid);
                ObjectOutputStream toBOut = BSession.objectOutputStream;
                toBOut.writeObject(toB);
                toBOut.flush();
                logger.debug("发送消息：目标：[" + BSession.clientInfo.ip + ":" + BSession.clientInfo.port + "]，信息：" + toB);
                break;
            case 3: // B："这是为A（UUID）准备的socket"。中心分别告诉A\B 说可以开始了，然后断开与A/B的连接
                String AUUID = (String) tcpMessage.data;
                logger.debug("处理请求：[" + clientSession.clientInfo.ip + ":" + clientSession.clientInfo.port + "]：" + " B：\"这是为A（UUID）准备的socket\"。");
                // Center通知A："我联系上B了，现在给你B的IP/Port，你可以连接B了。"
                TcpMessage toATcpMessage = new TcpMessage();
                toATcpMessage.code = 2001;
                toATcpMessage.msg = "我联系上B了，现在给你B的IP/Port，你可以连接B了。";
                Object[] dataArray = new Object[2]; // 这里保险，没有List,数组默认可以序列化的
                dataArray[0] = clientSession.clientInfo.ip;
                dataArray[1] = clientSession.clientInfo.port;
                toATcpMessage.data = dataArray;
                ClientSession ASession = sessionMap.get(AUUID);
                ObjectOutputStream toAOut = ASession.objectOutputStream;
                toAOut.writeObject(toATcpMessage);
                toAOut.flush();
                logger.debug("发送消息：目标：[" + ASession.clientInfo.ip + ":" + ASession.clientInfo.port + "]，信息：" + toATcpMessage);
                // 中央与A断开连接
                closeSocket(ASession);

                // Center通知B："A已知道你准备好了，你可以连接A了"
                TcpMessage toBMsg = new TcpMessage();
                toBMsg.code = 2002;
                toBMsg.msg = "A已知道你准备好了，你可以连接A了";
                toBMsg.data = clientSession.clientInfo.port;// B自己的外网IP
                clientSession.objectOutputStream.writeObject(toBMsg);
                clientSession.objectOutputStream.flush();
                logger.debug("发送消息：目标：[" + clientSession.clientInfo.ip + ":" + clientSession.clientInfo.port + "]，信息：" + toBMsg);
                // 中央与B断开连接
                closeSocket(clientSession);
                break;
            case 4:
                /*
                连接用户流程：(新) 因为:tcp socket 可以直接互相连接，不需要一方使用serverSocket
                1、A从中央获取有那些用户，并且获取常驻通讯
                2、A新建一个Socket与Center建立连接，并告诉中央要连接B，通过B的UUID
                3、中央记录A外网IP与端口，并为这个socket新newUUID,通知B：uuid,ip,port,需要与你建立连接。
                4、B新建一个Socket与Center建立连接，告诉中央：这个连接，是为UUID的A连接做准备的。
                5、中央记录B的ip,port。通知A：“B联系上了，它的ip\port，请做好准备”。通知B：“A已通知，请连接它”(通知完后，Center也要关闭与他们的连接)
                6、A开启线程直接连接B
                7、B开启线程直接连接B
                8、成功！
                 */
                /*
                连接用户流程：(旧)
                1、A从中央获取有那些用户，并且获取常驻通讯
                2、A新建一个Socket与Center建立连接，并告诉中央要连接B，通过B的UUID
                3、中央记录A外网IP与端口，并为这个socket新newUUID,通知B：uuid,ip,port,需要与你建立连接。
                4、B新建一个Socket与Center建立连接，告诉中央：这个连接，是为UUID的A连接做准备的。
                5、中央记录ip,port。通知A：B联系上了，它的ip\port，请做好准备。通知B：A已通知(通知完后，Center也要关闭与他们的连接)，请连接它
                6、A先试探的用原端口，请求下B5次，100ms一次（为了让NAR给B留个记录）
                7、A用原端口创建ServerSocket
                8、500ms后，B用指定原端口尝试连接5次，400ms一次，如果还是失败，就用再该端口通过中央告诉原主机。
                9、一般这个时候就已经连接成功，先测试，如果成功就不考虑后面
                 */
                break;
            case 5: // 获取外网IP与端口
                TcpMessage tcpMessage5 = new TcpMessage();
                tcpMessage5.code = 1003;
                tcpMessage5.msg = "你要的外网IP与端口，请拿去。";
                Object[] data5 = {clientSession.clientInfo.ip, clientSession.clientInfo.port};
                tcpMessage5.data = data5;
                clientSession.objectOutputStream.writeObject(tcpMessage5);
                clientSession.objectOutputStream.flush();
                break;
            case 6: // 断开连接
                closeSocket(clientSession);
        }
    }

    /**
     * 关闭与某的SocketClient的连接
     */
    public void closeSocket(ClientSession clientSession) throws IOException {
        // 线程标记关闭
        clientSession.isActive = false;
        // socket in out 流关闭
        clientSession.objectOutputStream.close();
        clientSession.objectInputStream.close();
        clientSession.socket.close();
        // 相关数据结构维护
        clientInfos.remove(clientSession.clientInfo);
        sessionMap.remove(clientSession.uuid);
        logger.info("断开连接：" + clientSession.clientInfo.ip + ":" + clientSession.clientInfo.port);
    }
}
// 未处理BUG
/* TODO 来呀，互相伤害啊
1、自己申请连接自己问题，其他而言不会有这个问题，对服务器也没影响，可以不管
2、tcp打洞没建立验证机制，一般来说port都是随机指定的。展示不要验证机制
 */